/*===========================================================================
  Copyright (C) 2014 by the Okapi Framework contributors
-----------------------------------------------------------------------------
  This library is free software; you can redistribute it and/or modify it 
  under the terms of the GNU Lesser General Public License as published by 
  the Free Software Foundation; either version 2.1 of the License, or (at 
  your option) any later version.

  This library is distributed in the hope that it will be useful, but 
  WITHOUT ANY WARRANTY; without even the implied warranty of 
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 
  General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License 
  along with this library; if not, write to the Free Software Foundation, 
  Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

  See also the full LGPL text here: http://www.gnu.org/copyleft/lesser.html
===========================================================================*/

package net.sf.okapi.lib.xliff2.validation;

import java.text.Normalizer;
import java.text.Normalizer.Form;

import net.sf.okapi.lib.xliff2.InvalidParameterException;
import net.sf.okapi.lib.xliff2.XLIFFException;
import net.sf.okapi.lib.xliff2.core.ExtAttributes;
import net.sf.okapi.lib.xliff2.core.IWithExtAttributes;

/**
 * Represents the &lt;rule> element of the 
 * <a href='http://docs.oasis-open.org/xliff/xliff-core/v2.0/xliff-core-v2.0.html#validation_module'>Validation module</a>.
 */
public class Rule implements IWithExtAttributes {

	/**
	 * Types of validation rule.
	 */
	public enum Type {
		ISPRESENT("isPresent"),
		ISNOTPRESENT("isNotPresent"),
		STARTSWITH("startsWith"),
		ENDSWITH("endsWith"),
		CUSTOM("custom");

		private String name;

		private Type (String name) {
			this.name = name;
		}

		@Override
		public String toString () {
			return name;
		}

		/**
		 * Converts a type from a string to an object.
		 * @param name the name of the type as a string.
		 * @return the type as an object.
		 * @throws InvalidParameterException if the name is invalid.
		 */
		public static Type fromString (String name) {
			if ( name == null ) {
				throw new InvalidParameterException("A rule type must not be null");
			}
			switch ( name ) {
			case "isPresent":
				return ISPRESENT;
			case "isNotPresent":
				return ISNOTPRESENT;
			case "startsWith":
				return STARTSWITH;
			case "endsWith":
				return ENDSWITH;
			// "custom" is not a valid value
			// Other values are invalid
			default:
				throw new InvalidParameterException(String.format("Invalid rule type value: '%s'.", name));
			}
		}
	};

	/**
	 * Possible forms of normalization.
	 */
	public enum Normalization {
		NONE("none"),
		NFC("nfc"),
		NFD("nfd");

		private String name;

		private Normalization (String name) {
			this.name = name;
		}

		@Override
		public String toString () {
			return name;
		}
		
		/**
		 * Converts a normalization form from a string to an object.
		 * @param name the name of the form as a string.
		 * @return the form as an object.
		 * @throws InvalidParameterException if the name is invalid.
		 */
		public static Normalization fromString (String name) {
			if ( name == null ) {
				throw new InvalidParameterException("A normalization value must not be null");
			}
			switch ( name ) {
			case "none":
				return NONE;
			case "nfc":
				return NFC;
			case "nfd":
				return NFD;
			default:
				throw new InvalidParameterException(String.format("Invalid normalization value: '%s'.", name));
			}
		}
	};

	private Type type = null;
	private String data;
	private String effectiveData;
	private int occurs;
	private boolean existsInSource = false;
	private boolean caseSensitive = true;
	private boolean enable = true;
	private Normalization normalization = Normalization.NFC;
	private ExtAttributes xattrs;
	private boolean inherited = false;

	/**
	 * Creates a rule of a given type.
	 * @param type the name of the rule type.
	 * @param data the text data for the rule (can be null).
	 */
	public Rule (String type,
		String data)
	{
		this.type = Type.fromString(type);
		this.data = data;
	}
	
	/**
	 * Copy constructor.
	 * @param original the original object to duplicate.
	 */
	public Rule (Rule original) {
		type = original.type;
		caseSensitive = original.caseSensitive;
		normalization = original.normalization;
		data = original.data;
		effectiveData = original.effectiveData;
		enable = original.enable;
		existsInSource = original.existsInSource;
		occurs = original.occurs;
		inherited = original.inherited;
		if ( original.hasExtAttribute() ) {
			xattrs = new ExtAttributes(original.xattrs);
		}
	}

	/**
	 * Gets a human readable representation of the rule.
	 * @return the text display of this rule.
	 */
	public String getDisplay () {
		StringBuilder tmp = new StringBuilder(type.toString() + "='"+data+"'"
			+ (" caseSensitive="+(caseSensitive ? "yes" : "no"))
			+ (" normalization="+normalization.toString()));
		// type-specific info
		switch ( type ) {
		case ISPRESENT:
			tmp.append(occurs>1 ? " occurs="+occurs : " occurs=once-or-more");
			// Fall thru
		case ENDSWITH:
		case STARTSWITH:
			tmp.append(" existsInSource=" + (existsInSource ? "yes" : "no"));
			break;
		case CUSTOM:
		case ISNOTPRESENT:
			break;
		}
		tmp.append(inherited ? " (inherited-rule)" : "");
		return tmp.toString();
	}

	/**
	 * Gets the type of this rule.
	 * @return the type of this rule.
	 */
	public Type getType () {
		return type;
	}

	/**
	 * Sets the type of this rule.
	 * @param type the new type of this rule.
	 */
	public void setType (Type type) {
		this.type = type;
	}

	/**
	 * Gets the data (text) of this rule.
	 * @return the data of this rule.
	 */
	public String getData () {
		return data;
	}

	/**
	 * Sets the data (text) of this rule.
	 * @param data the new data of this rule.
	 */
	public void setData (String data) {
		this.data = data;
	}

	/**
	 * Gets the effective text to use when applying the rule.
	 * For example if the rule is not case sensitive or need to be normalized, the original
	 * data string needs to be modified for applying the rule. The effective data is the
	 * result of those changes.
	 * @return the effective data for this rule.
	 */
	public String getEffectiveData () {
		return effectiveData;
	}

	/**
	 * Gets the number of times the text must occur.
	 * @return the number of times the text must occur.
	 */
	public int getOccurs () {
		return occurs;
	}

	/**
	 * Sets the number of times the text must occur.
	 * @param occurs the new number of times the text must occur.
	 */
	public void setOccurs (int occurs) {
		this.occurs = occurs;
	}

	/**
	 * Gets the flag 'existsinSource'.
	 * @return the flag 'existsinSource'
	 */
	public boolean getExistsInSource () {
		return existsInSource;
	}

	/**
	 * Sets the flag 'existsinSource'
	 * @param existsInSource the flag 'existsinSource' to set
	 */
	public void setExistsInSource (boolean existsInSource) {
		this.existsInSource = existsInSource;
	}

	/**
	 * Gets the flag 'caseSensitive'.
	 * @return the flag 'caseSensitive'.
	 */
	public boolean isCaseSensitive () {
		return caseSensitive;
	}

	/**
	 * Sets the flag 'caseSensitive'.
	 * @param caseSensitive the flag 'caseSensitive' to set.
	 */
	public void setCaseSensitive (boolean caseSensitive) {
		this.caseSensitive = caseSensitive;
	}

	/**
	 * Indicates if this rule is enabled.
	 * @return true if this rule is enabled, false if it is disabled.
	 */
	public boolean isEnable () {
		return enable;
	}

	/**
	 * Sets the flag indicating if this rule is enabled.
	 * @param enable true to enable the rule, false to disabled it.
	 */
	public void setEnable (boolean enable) {
		this.enable = enable;
	}

	/**
	 * Gets the normalization form to use for this rule.
	 * @return the normalization form to use for this rule.
	 */
	public Normalization getNormalization () {
		return normalization;
	}

	/**
	 * Sets the normalization form to use for this rule.
	 * @param normalization the new normalization form to use for this rule.
	 */
	public void setNormalization (Normalization normalization) {
		this.normalization = normalization;
	}

	@Override
	public void setExtAttributes (ExtAttributes attributes) {
		this.xattrs = attributes;
	}

	/**
	 * Indicates if this rule is inherited (defined in a parent).
	 * @return true if this rule is inherited, false if not.
	 */
	public boolean isInherited () {
		return inherited;
	}

	/**
	 * Sets the flag indicating if this rule is inherited.
	 * @param inherited true if the rule is inherited, false if not.
	 */
	public void setInherited (boolean inherited) {
		this.inherited = inherited;
	}

	@Override
	public ExtAttributes getExtAttributes () {
		if ( xattrs == null ) {
			xattrs = new ExtAttributes();
		}
		return xattrs;
	}

	@Override
	public boolean hasExtAttribute () {
		if ( xattrs == null ) return false;
		return !xattrs.isEmpty();
	}

	@Override
	public String getExtAttributeValue (String namespaceURI,
		String localName)
	{
		if ( xattrs == null ) return null;
		return xattrs.getAttributeValue(namespaceURI, localName);
	}
	
	/**
	 * Applies the options such as case sensitivity and normalization to the data of this rule.
	 * This updates the value returned by {@link #getEffectiveData()}. The value returned by
	 * {@link #getData()} is untouched.
	 */
	public void prepare () {
		verify();
		if ( type == Type.CUSTOM ) return;
		effectiveData = data;
		if ( !isCaseSensitive() ) effectiveData = effectiveData.toLowerCase();
		switch ( normalization ) {
		case NFC:
			effectiveData = Normalizer.normalize(effectiveData, Form.NFC);
			break;
		case NFD:
			effectiveData = Normalizer.normalize(effectiveData, Form.NFD);
			break;
		case NONE:
			// Do nothing
			break;
		}
	}

	/**
	 * Verifies if this rule has valid parameters.
	 * @throws XLIFFException if there is a problem with the rule's parameters.
	 */
	public void verify () {
		if ( type == null ) {
			throw new XLIFFException("You must specify a type of rule (e.g. isPresent, startsWith, etc.)");
		}
		switch ( type ) {
		case CUSTOM:
			// TBD
//			if ( getExtAttributes().size() != 1 ) {
//				// Rule must have one and only one extended attribute
//				throw new RuntimeException("A custom rule must have a single extension attribute.");
//			}
			break;
		case ENDSWITH:
			break;
		case ISNOTPRESENT:
			break;
		case ISPRESENT:
			break;
		case STARTSWITH:
			break;
		}

		// Note: existsInSource must be used only with
		// isPresent, startsWith and endsWith

		// Note: occurs is used only with isPresent, but there is no restriction in the specification
		// to have it forbidden with other type of rules
	}

	/**
	 * Indicates if this rule is "the same" as a given one.
	 * @param other the other rule to compare this with.
	 * @return true if both rule are "the same".
	 */
	public boolean sameAs (Rule other) {
		if ( other.getType() != this.getType() ) return false;
		if ( other.isCaseSensitive() != this.isCaseSensitive() ) return false;
		if ( other.getNormalization() != this.getNormalization() ) return false;
		switch ( getType() ) {
		case CUSTOM:
		case ISNOTPRESENT:
			// Nothing specific to check
			break;
		case ISPRESENT:
			if ( other.getOccurs() != this.getOccurs() ) return false;
			// Else: fall thru
		case ENDSWITH:
		case STARTSWITH:
			if ( other.getExistsInSource() != this.getExistsInSource() ) return false;
			break;
		}
		// Then compare the effective data
		other.prepare(); this.prepare();
		if ( !other.getEffectiveData().equals(this.getEffectiveData()) ) return false;
		// Both are the 'same'
		return true;
	}

}
